package cc.shacocloud.redis.study.util;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.jetbrains.annotations.NotNull;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

/**
 * Kryo 序列化工具
 */
public class KryoUtil {

    private static final String DEFAULT_ENCODING = "UTF-8";

    /**
     * 每个线程的 Kryo 实例
     * <p>
     * 不要轻易改变这里的配置！更改之后，序列化的格式就会发生变化，
     * 上线的同时就必须清除 Redis 里的所有缓存，
     * 否则那些缓存再回来反序列化的时候，就会报错
     */
    private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        //支持对象循环引用（否则会栈溢出）
        kryo.setReferences(true); //默认值就是 true，添加此行的目的是为了提醒维护者，不要改变这个配置
        //不强制要求注册类（注册行为无法保证多个 JVM 内同一个类的注册编号相同；而且业务系统中大量的 Class 也难以一一注册）
        kryo.setRegistrationRequired(false); //默认值就是 false，添加此行的目的是为了提醒维护者，不要改变这个配置
        //在反序列化集合时修复NPE错误.
        ((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()).setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
        return kryo;
    });

    /**
     * 利用序列化进行对象深克隆
     *
     * @param object 任意对象
     * @param <T>    任意对象
     * @return 任意对象的克隆对象
     * @throws IOException 例外
     */
    public static <T> T clone(T object) throws IOException {
        return KryoUtil.readFromByteArray(KryoUtil.writeToByteArray(object));
    }

    /**
     * 获得当前线程的 Kryo 实例
     *
     * @return 当前线程的 Kryo 实例
     */
    public static Kryo getInstance() {
        return kryoLocal.get();
    }

    //-----------------------------------------------
    //          序列化/反序列化对象，及类型信息
    //          序列化的结果里，包含类型的信息
    //          反序列化时不再需要提供类型
    //-----------------------------------------------

    /**
     * 将对象【及类型】序列化为字节数组
     * <p>
     * 使用{@linkplain #readFromByteArray(byte[])} 反序列化
     *
     * @param obj 任意对象
     * @return 序列化后的字节数组
     * @throws IOException 序列化发生例外
     */
    @NotNull
    public static byte[] writeToByteArray(Object obj) throws IOException {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = getInstance();
            kryo.writeClassAndObject(output, obj);
            output.flush();
            return byteArrayOutputStream.toByteArray();
        }
    }


    /**
     * 将字节数组反序列化为原对象
     * <p>
     * 使用{@linkplain #writeToByteArray(Object)}序列化
     *
     * @param byteArray writeToByteArray 方法序列化后的字节数组
     * @param <T>       原对象的类型
     * @return 原对象
     * @throws IOException 反序列化发生例外
     */
    @SuppressWarnings("unchecked")
    public static <T> T readFromByteArray(byte[] byteArray) throws IOException {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = getInstance();
            return (T) kryo.readClassAndObject(input);
        }
    }

    /**
     * 将对象【及类型】序列化为 String
     * 利用了 Base64 编码
     * <p>
     * 使用{@linkplain #readFromString(String)}反序列化
     *
     * @param obj 任意对象
     * @return 序列化后的字符串
     * @throws IOException 序列化发生例外
     */
    @NotNull
    public static String writeToString(Object obj) throws IOException {
        byte[] bytes = writeToByteArray(obj);
        if (bytes.length == 0) return "";
        return new String(Base64.getEncoder().encode(bytes), DEFAULT_ENCODING);
    }

    /**
     * 将 String 反序列化为原对象
     * 利用了 Base64 编码
     * <p>
     * 使用{@linkplain #writeToString(Object)}序列化
     *
     * @param str writeToString 方法序列化后的字符串
     * @param <T> 原对象的类型
     * @return 原对象
     * @throws IOException 反序列化发生例外
     */
    public static <T> T readFromString(@NotNull String str) throws IOException {
        return readFromByteArray(Base64.getDecoder().decode(str.getBytes(DEFAULT_ENCODING)));
    }

    //-----------------------------------------------
    //          只序列化/反序列化对象
    //          序列化的结果里，不包含类型的信息
    //-----------------------------------------------

    /**
     * 将对象序列化为字节数组
     * <p>
     * 利用{@linkplain #readObjectFromByteArray(byte[], Class)}反序列化
     *
     * @param obj 任意对象
     * @return 序列化后的字节数组
     * @throws IOException 序列化发生例外
     */
    @NotNull
    public static byte[] writeObjectToByteArray(Object obj) throws IOException {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = getInstance();
            kryo.writeObject(output, obj);
            output.flush();
            return byteArrayOutputStream.toByteArray();
        }
    }

    /**
     * 将字节数组反序列化为原对象
     * <p>
     * 利用{@linkplain #writeObjectToByteArray(Object)}序列化
     *
     * @param byteArray writeObjectToByteArray 方法序列化后的字节数组
     * @param clazz     原对象的 Class
     * @param <T>       原对象的类型
     * @return 原对象
     * @throws IOException 反序列化发生例外
     */
    public static <T> T readObjectFromByteArray(byte[] byteArray, Class<T> clazz) throws IOException {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = getInstance();
            return kryo.readObject(input, clazz);
        }
    }

    /**
     * 将对象序列化为 String
     * 利用了 Base64 编码
     * <p>
     * 利用{@linkplain #readObjectFromString(String, Class)} 反序列化
     *
     * @param obj 任意对象
     * @return 序列化后的字符串
     * @throws IOException 序列化发生例外
     */
    @NotNull
    public static String writeObjectToString(Object obj) throws IOException {
        byte[] bytes = writeObjectToByteArray(obj);
        if (bytes.length == 0) return "";
        return new String(Base64.getEncoder().encode(bytes), DEFAULT_ENCODING);
    }

    /**
     * 将 String 反序列化为原对象
     * 利用了 Base64 编码
     * <p>
     * 利用{@linkplain #writeObjectToString(Object)} 序列化
     *
     * @param str   writeToString 方法序列化后的字符串
     * @param clazz 原对象的 Class
     * @param <T>   原对象的类型
     * @return 原对象
     * @throws IOException 反序列化发生例外
     */
    public static <T> T readObjectFromString(@NotNull String str, Class<T> clazz) throws IOException {
        return readObjectFromByteArray(Base64.getDecoder().decode(str.getBytes(DEFAULT_ENCODING)), clazz);
    }
}
